/**
 * Copyright 2015 - 2016 KeepSafe Software, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.getkeepsafe.relinker.elf;


import java.io.*;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Function description
 * class ElfParser
 *
 */
public class ElfParser implements Closeable, Elf {
    private FileChannel channel = null;

    /**
     * Constructor
     *
     * @param file File
     */
    public ElfParser(final File file) {
        if (file == null || !file.exists()) {
            throw new IllegalArgumentException("File is null or does not exist");
        }
        FileInputStream inputStream = null;
        try {
            inputStream = new FileInputStream(file);
            this.channel = inputStream.getChannel();
        } catch (FileNotFoundException e) {
            LogUtils.info(Constants.BUNDLE_NAME, Constants.TAG_VAL);
        } finally {
            closeSilently(inputStream);
            closeSilently(channel);
        }
    }

    /**
     * Print info log
     *
     * @return Elf.Header
     */
    public Header parseHeader() {
        try {
            channel.position(0L);
            final ByteBuffer buffer = ByteBuffer.allocate(8);
            buffer.order(ByteOrder.LITTLE_ENDIAN);
            if (readWord(buffer, 0) != Constants.ELF_PARSER_MAGIC) {
                throw new IllegalArgumentException("Invalid ELF Magic!");
            }

            final short fileClass = readByte(buffer, Constants.READ_BUFFER_ELF);
            final boolean bigEndian = readByte(buffer, Constants.READ_BIGENDIAN) == Header.ELFDATA2MSB;
            if (fileClass == Header.ELFCLASS32) {
                return new Elf32Header(bigEndian, this);
            } else if (fileClass == Header.ELFCLASS64) {
                return new Elf64Header(bigEndian, this);
            } else {
                LogUtils.info(Constants.BUNDLE_NAME, "parseHeader fileClass is not valid");
            }
        } catch (IOException e) {
            LogUtils.info(Constants.BUNDLE_NAME, Constants.TAG_VAL);
        }
        throw new IllegalStateException("Invalid class type!");
    }

    /**
     * parseNeededDependencies
     *
     * @return dependencies List
     */
    public List<String> parseNeededDependencies() {
        final List<String> dependencies = new ArrayList<String>();
        try {
            channel.position(0);
            final Header header = parseHeader();
            final ByteBuffer buffer = ByteBuffer.allocate(8);
            buffer.order(header.mIsBigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN);
            long numProgramHeaderEntries = header.mPhnum;
            if (numProgramHeaderEntries == Constants.READ_NUM_PROGRAM) {
                final SectionHeader sectionHeader = header.getSectionHeader(0);
                numProgramHeaderEntries = sectionHeader.info;
            }

            long dynamicSectionOff = 0L;
            for (long ivalue = 0L; ivalue < numProgramHeaderEntries; ++ivalue) {
                final ProgramHeader programHeader = header.getProgramHeader(ivalue);
                if (programHeader.type == ProgramHeader.PT_DYNAMIC) {
                    dynamicSectionOff = programHeader.offset;
                    break;
                }
            }

            if (dynamicSectionOff == 0) {
                // No dynamic linking info, nothing to load
                return Collections.unmodifiableList(dependencies);
            }

            int ival = Constants.COPY_NUM_CONST;
            final List<Long> neededOffsets = new ArrayList<Long>();
            long vstringtableoff = 0L;
            DynamicStructure dynStructure;
            do {
                dynStructure = header.getDynamicStructure(dynamicSectionOff, ival);
                if (dynStructure.tag == DynamicStructure.DT_NEEDED) {
                    neededOffsets.add(dynStructure.val);
                } else if (dynStructure.tag == DynamicStructure.DT_STRTAB) {
                    vstringtableoff = dynStructure.val; // d_ptr union
                } else {
                    LogUtils.info(Constants.BUNDLE_NAME, "parseNeededDependencies tag is not valid");
                }
                ++ival;
            } while (dynStructure.tag != DynamicStructure.DT_NULL);

            if (vstringtableoff == 0) {
                throw new IllegalStateException("String table offset not found!");
            }

            // Map to file offset
            final long stringTableOff = offsetFromVma(header, numProgramHeaderEntries, vstringtableoff);
            for (final long strOff : neededOffsets) {
                dependencies.add(readString(buffer, stringTableOff + strOff));
            }
        } catch (IOException e) {
            LogUtils.info(Constants.BUNDLE_NAME, Constants.TAG_VAL);
        }
        return dependencies;
    }

    /**
     * method offsetFromVma
     *
     * @param header E1f.Header
     * @param numEntries long
     * @param vma long
     * @return long
     */
    private long offsetFromVma(final Header header, final long numEntries, final long vma) {
        try {
            for (long index = 0L; index < numEntries; ++index) {
                final ProgramHeader programHeader = header.getProgramHeader(index);
                if (programHeader.type == ProgramHeader.PT_LOAD) {
                    // Within memsz instead of filesz to be more tolerant
                    if (programHeader.vaddr <= vma && vma <= programHeader.vaddr + programHeader.memsz) {
                        return vma - programHeader.vaddr + programHeader.offset;
                    }
                }
            }
        } catch (IOException e) {
            LogUtils.info(Constants.BUNDLE_NAME, Constants.TAG_VAL);
        }

        throw new IllegalStateException("Could not map vma to file offset!");
    }

    @Override
    public void close() throws IOException {
        this.channel.close();
    }

    /**
     * method readString
     *
     * @param buffer ByteBuffer
     * @param offset long
     * @return String
     * @throws IOException
     */
    protected String readString(final ByteBuffer buffer, long offset) {
        final StringBuilder builder = new StringBuilder();
        short cval;
        long localOffset = offset;
        while ((cval = readByte(buffer, localOffset++)) != 0) {
            builder.append((char) cval);
        }
        return builder.toString();
    }

    /**
     * Method readLong
     *
     * @param buffer ByteBuffer
     * @param offset long
     * @return long
     */
    protected long readLong(final ByteBuffer buffer, final long offset) {
        read(buffer, offset, Constants.BUFFER_OFFSET_LONG_ID_CONSTANT);
        return buffer.getLong();
    }

    /**
     * Method readWord
     *
     * @param buffer ByteBuffer
     * @param offset long
     * @return long
     */
    protected long readWord(final ByteBuffer buffer, final long offset) {
        read(buffer, offset, Constants.EIL_VAL);
        return buffer.getInt() & Constants.READ_NUM_RETURN;
    }

    /**
     * method readHalf
     *
     * @param buffer ByteBuffer
     * @param offset long
     * @return integer
     */
    protected int readHalf(final ByteBuffer buffer, final long offset) {
        read(buffer, offset, Constants.READ_HALF_VAL);
        return buffer.getShort() & Constants.GET_SHOT_RETURN_VAL;
    }

    /**
     * method readByte
     *
     * @param buffer ByteBuffer
     * @param offset long
     * @return integer
     */
    protected short readByte(final ByteBuffer buffer, final long offset) {
        read(buffer, offset, 1);
        return (short) (buffer.get() & Constants.EIL_RETURN_VAL);
    }

    /**
     * method readByte
     *
     * @param buffer ByteBuffer
     * @param offset long
     * @param length integer
     */
    protected void read(final ByteBuffer buffer, long offset, final int length) {
        try {
            buffer.position(0);
            buffer.limit(length);
            long bytesRead = 0L;
            while (bytesRead < length) {
                final int read = channel.read(buffer, offset + bytesRead);
                if (read == Constants.READ_NUM_CONST) {
                    throw new EOFException();
                }

                bytesRead += read;
            }
            buffer.position(0);
        } catch (IOException e) {
            LogUtils.info(Constants.BUNDLE_NAME, Constants.TAG_VAL);
        }
    }

    /**
     * Method closeSilently
     *
     * @param closeable Context
     */
    private void closeSilently(final Closeable closeable) {
        try {
            if (closeable != null) {
                closeable.close();
            }
        } catch (IOException ignored) {
            LogUtils.info(Constants.BUNDLE_NAME, "ignore");
        }
    }
}